import { miniAppsMessage, pipeJsonToSchema, themeParams } from '@tma.js/transformers';
import {
  boolean,
  looseObject,
  nullish,
  number,
  optional,
  parse,
  string,
  unknown,
  type BaseSchema,
} from 'valibot';

import { createEmitter } from '@/events/createEmitter.js';
import { emitEvent } from '@/events/emitEvent.js';
import type { EventName, EventPayload, Events } from '@/events/types/index.js';
import { logger } from '@/globals.js';
import { defineFnComposer, defineMergeableProperty } from '@/obj-prop-helpers.js';

/**
 * Transformers for problematic Mini Apps events.
 */
const transformers = {
  clipboard_text_received: looseObject({
    req_id: string(),
    data: nullish(string()),
  }),
  custom_method_invoked: looseObject({
    req_id: string(),
    result: optional(unknown()),
    error: optional(string()),
  }),
  popup_closed: nullish(
    looseObject({ button_id: nullish(string(), () => undefined) }),
    {},
  ),
  viewport_changed: nullish(
    looseObject({
      height: number(),
      width: nullish(number(), () => window.innerWidth),
      is_state_stable: boolean(),
      is_expanded: boolean(),
    }),
    // TODO: At the moment, macOS has a bug with the invalid event payload - it is always equal to
    //  null. Leaving this default value until the bug is fixed.
    () => ({
      height: window.innerHeight,
      is_state_stable: true,
      is_expanded: true,
    }),
  ),
  theme_changed: looseObject({
    theme_params: themeParams(),
  }),
} as const satisfies { [E in EventName]?: BaseSchema<unknown, EventPayload<E>, any> };

function windowMessageListener(event: MessageEvent): void {
  // Ignore non-parent window messages.
  if (event.source !== window.parent) {
    return;
  }

  // Parse incoming event data.
  let message: { eventType: string; eventData?: unknown };
  try {
    message = parse(pipeJsonToSchema(miniAppsMessage()), event.data);
  } catch {
    // We ignore incorrect messages as they could be generated by any other code.
    return;
  }

  const { eventType, eventData } = message;
  const schema = transformers[eventType as keyof typeof transformers];

  let data: unknown;
  try {
    data = schema ? parse(schema, eventData) : eventData;
  } catch (cause) {
    return logger().forceError(
      [
        `An error occurred processing the "${eventType}" event from the Telegram application.`,
        'Please, file an issue here:',
        'https://github.com/Telegram-Mini-Apps/tma.js/issues/new/choose',
      ].join('\n'),
      message,
      cause,
    );
  }
  emit(eventType as any, data);
}

export const {
  on,
  off,
  emit,
  clear: offAll,
} = createEmitter<Events>(
  () => {
    const wnd = window as any;

    // Define all functions responsible for receiving an event from the Telegram client.
    // All these "ports" should narrow the communication way to a single specific one - the way
    // accepted by the web version of Telegram between iframes.
    //
    // Here we consider 2 cases:
    // 1. When the Telegram SDK is already connected. In this case the Telegram SDK already
    // installed its own ports, and we should rewire them. The cleanup function should also work
    // properly in this context, removing @tma.js/bridge handler only, not
    // the Telegram SDK one.
    // 2. When the Telegram SDK is not connected, but probably will be. We know, that
    // the Telegram SDK is going to overwrite our own handlers. Due to this reason, we should
    // protect them from being overwritten, but still support handlers defined by the Telegram SDK.

    // TelegramGameProxy.receiveEvent
    !wnd.TelegramGameProxy && (wnd.TelegramGameProxy = {});
    defineFnComposer(wnd.TelegramGameProxy, 'receiveEvent', emitEvent);
    defineMergeableProperty(wnd, 'TelegramGameProxy');

    // Telegram.WebView.receiveEvent
    !wnd.Telegram && (wnd.Telegram = {});
    !wnd.Telegram.WebView && (wnd.Telegram.WebView = {});
    defineFnComposer(wnd.Telegram.WebView, 'receiveEvent', emitEvent);
    defineMergeableProperty(wnd.Telegram, 'WebView');

    // TelegramGameProxy_receiveEvent
    defineFnComposer(wnd, 'TelegramGameProxy_receiveEvent', emitEvent);

    // Add a listener handling events sent from the Telegram web application and also events
    // generated by the local emitEvent function.
    // This handler should emit a new event using the library event emitter.
    window.addEventListener('message', windowMessageListener);
  },
  () => {
    [
      ['TelegramGameProxy_receiveEvent'],
      ['TelegramGameProxy', 'receiveEvent'],
      ['Telegram', 'WebView', 'receiveEvent'],
    ].forEach(path => {
      const wnd = window as any;

      // A tuple, where the first value is the receiveEvent function owner, and the second
      // value is the receiveEvent itself.
      let cursor: [obj: any, receieveEvent: any] = [undefined, wnd];
      for (const item of path) {
        cursor = [cursor[1], cursor[1][item]];
        if (!cursor[1]) {
          return;
        }
      }
      const [receiveEventOwner, receiveEvent] = cursor;
      if ('unwrap' in receiveEvent) {
        receiveEvent.unwrap();
        if (
          receiveEventOwner
          && receiveEventOwner !== wnd
          && !Object.keys(receiveEventOwner).length
        ) {
          delete wnd[path[0]];
        }
      }
    });
    window.removeEventListener('message', windowMessageListener);
  },
);
